<!DOCTYPE html>
<html lang="en">
<!--
 Copyright 2022 The Chromium Authors
 Use of this source code is governed by a BSD-style license that can be
 found in the LICENSE file.
  -->

<head>
  <style>
    #scrubberframe::-webkit-slider-thumb {
      appearance: none;
      width: 20px;
      height: 20px;
      background-color: grey;
    }

    #scrubberframe {
      margin-top: 10px;
      flex-grow: 1;
      appearance: none;
      background-color: #f0f0f0;
      width: 100%;
    }

    .scrubber::-webkit-slider-thumb {
      appearance: none;
      width: 20px;
      height: 20px;
      background-color: grey;
    }

    .minMaxScrubber {
      width: 100%;
      display: flex;
    }

    .minMaxScrubber > .scrubber {
      appearance: none;
      background-color: #f0f0f0;
      margin: 0px;
      flex-grow: 1;
      flex-basis: 20px; /* width of slider-thumb */
      min-width: 0px;
    }

    #url {
      font-family: monospace;
      font-size: smaller;
    }

    #controls>#buttons {
      display: flex;
    }

    #log {
      min-height: 100px;
      max-height: 100%;
      font-family: monospace;
      overflow: auto;
    }

    #connectionPanel,
    #saveload {
      display: inline-block;
    }

    #connectionPanel,
    #saveload,
    #topPanel {
      padding-bottom: 10px;
    }

    #topPanel {
      display: flex;
    }

    #settings {
      display: flex;
      flex-direction: column;
      font-size: small;
    }

    .panelSection {
      margin-right: 20px;
    }

    .panelSection:last-child {
      margin-right: 0px;
      flex-grow: 1;
    }

    #connection-status {
      color: limegreen;
      font-size: large;
      padding-right: 5px;
    }

    #connection-status.disconnected {
      color: orange;
    }
  </style>
  <link href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css" rel="stylesheet">
  <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" />

  <link rel='stylesheet' href='style.css'>
  <script src='filter.js'></script>
  <script src='filter-ui.js'></script>
  <script src='frame.js'></script>
  <script src='connection.js'></script>
  <script src='thread.js'></script>
  <script src='thread-ui.js'></script>
</head>

<div id='connectionPanel'>
  <div class='sectionTitle'>
    <font class='disconnected' id='connection-status'>&#9679;</font>Connection
  </div>
  <div class='section'>
    <div title="Expert usage only. Autoconnect should provide the dev tools websocket through /json/version discovery.">
      <input id='url' name='url' size=60 type="text"
        placeholder='WebSocket URL or leave empty for autoconnect...'></input>
    </div>
    <button class="mdc-button mdc-button--outline" id='connect'>
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Connect</span>
    </button>
    <button class="mdc-button mdc-button--outline" id='disconnect' disabled=true>
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Disconnect</span>
    </button>
    <input type="checkbox" id="autoconnect" name="autoconnect" checked="true">
    <label for="autoconnect" style="font-size:small; font:Roboto" > Autoconnect</label><br>
  </div>
</div>

<div id='saveload'>
  <div class='sectionTitle'>Save/Load ...</div>
  <div class='section'>
    <button id='demo' class="mdc-button mdc-button--outline">
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Load demo data</span>
    </button>
    <button id='savedata' class="mdc-button mdc-button--outline"
      title="Serializes current session stream to json file.">
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Save to disk</span>
    </button>
    <button id='loaddata' class="mdc-button mdc-button--outline"
      title="Deserializes json file of previous session and imports these frames into the App.">
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Load from disk</span>
    </button>
  </div>
</div>

<div id='logsPanel'>
  <div class='panelSection'>
    <div class='sectionTitle'>
      Logs
      <!--input size=40 placeholder='Comma separated filter ...'>(TODO)</input-->
    </div>
    <div class='section'>
      <div id='log'></div>
    </div>
  </div>
</div>

<div class='panelSection'>
  <div class='sectionTitle'
    title="Filter debug data stream. Filter operations occur in a left to right order with first match being applied.">
    <i class="material-icons-outlined">filter_list</i> Filters
  </div>
  <div class='section'>
    <div id='filters' class="mdc-chip-set mdc-chip-set--filter" role="grid"
      title="Filter debug data stream. Filter operations occur in a left to right order with first match being applied.">
    </div>
    <button class="mdc-button mdc-button--outline" id='createfilter'>
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label"><i class="material-icons-outlined">add_box</i> Add new filter</span>
    </button>
  </div>
</div>

<div class='panelSection'>
  <div class='sectionTitle'>
    <i class="material-symbols-outlined">
      airwave
    </i>
    Threads
  </div>

  <div id='threads' class='section'></div>
</div>

<div class='panelSection'>
  <div class='sectionTitle'>Viewer Controls</div>
  <div class='section' id='controls'>
    <div id='buttons'>
      <button class="mdc-button mdc-button--outline" id='prev'>
        <span class="mdc-button__label"><i class="material-icons-outlined">skip_previous</i> Previous frame</span>
      </button>
      <button class="mdc-button mdc-button--outline" id='play'>
        <div class="mdc-button__ripple"></div>
        <span class="mdc-button__label"><i class="material-icons-outlined">play_circle_outline</i> Play</span>
      </button>
      <button class="mdc-button mdc-button--outline" id='pause'>
        <div class="mdc-button__ripple"></div>
        <span class="mdc-button__label"><i class="material-icons-outlined">pause_circle_outline</i> Pause</span>
      </button>
      <button class="mdc-button mdc-button--outline" id='next'>
        <span class="mdc-button__label">Next frame <i class="material-icons-outlined">skip_next</i></span>
      </button>
      <button class="mdc-button mdc-button--raised" id='live'>
        <div class="mdc-button__ripple"></div>
        <span class="mdc-button__label">Live <i class="material-icons-outlined">fast_forward</i></span>
      </button>

    </div>
    <div class='panelSection'>
      <div class='sectionTitle'>Frame Selection</div>
      <input type='range' min='0' max='0' value='0' id='scrubberframe'></input>
    </div>
    <div class='panelSection'>
      <div class='sectionTitle'>Draw Selection<span id="drawRange"></span></div>
      <div class="minMaxScrubber">
        <input type="range" id='minDrawScrubber' class="scrubber"
            min="0" max="0" value="0" step="1"></input>
        <input type="range" id='maxDrawScrubber' class="scrubber"
            min="0" max="1" value="1" step="1"></input>
      </div>
    </div>
  </div>
</div>

<div>
  <div class='sectionTitle'>
    Viewer
  </div>
  <div style='float: right' class='sectionTitle'>
    Scale
    <select id="viewerscale">
      <option id="100pct">100%</option>
      <option id="50pct">50%</option>
      <option id="200pct">200%</option>
      <option id="freeCam" disabled>Free Camera</option>
    </select>
  </div>

  <div style='float: right' class='sectionTitle'>
  Orientation
    <select id="viewerorientation">
      <option id="0deg">0 deg clockwise</option>
      <option id="90deg">90 deg clockwise</option>
      <option id="180deg">180 deg clockwise</option>
      <option id="270deg">270 deg clockwise</option>
      <option id="hFlip">Horizontal Flip</option>
      <option id="vFlip">Vertical Flip</option>
    </select>
  </div>

  <div class='section'>
    <!--We need to fix viewer size to avoid scroll position change when
      multiple displays are present. crbug.com/1358526-->
    <div style="height:4000px;  border: 1px dotted gray; background-color: #f0f0f0">
        <canvas id='canvas' style="top :0px"></canvas>
    </div>
  </div>

  <div class='modalContainer'></div>
</div>

<div id='importtracing'>
  <div class='sectionTitle'>Tracing (Prototype)...</div>
  <div class='section'>
    <button id='importtracebutton' class="mdc-button mdc-button--outline"
      title="Import tracing data (json) into visual debugger app.">
      <div class="mdc-button__ripple"></div>
      <span class="mdc-button__label">Import Trace</span>
    </button>
  </div>
</div>

<script>
  function processIncomingFrame(json) {
    if (!json) return;

    new DrawFrame(json);
    Player.instance.onNewFrame();
  }

  async function testAnimate() {
    const f = await fetch('demo.json');
    const text = await f.text();
    const json = JSON.parse(text);
    for (const frame of json) {
      processIncomingFrame(frame);
    }
  }

  async function saveDemoDataToDisk() {
    const text = JSON.stringify(DrawFrame.frameBuffer.instances.map(d => d.toJSON()));
    const blob = new Blob([text], { type: 'text/plain' });
    const link = document.createElement('a');
    link.download = 'cvd-stream.json';
    link.href = window.URL.createObjectURL(blob);
    link.click();
  }

  window.onload = function () {

    Connection.initialize();

    const addFilterButton = document.querySelector('#createfilter');
    addFilterButton.addEventListener('click', () => {
      showCreateFilterPopup(addFilterButton);
    });

    const container = document.querySelector('.modalContainer');
    container.addEventListener('click', (event) => {
      if (event.target == container)
        hideModal();
    });

    const demo = document.querySelector('#demo');
    demo.addEventListener('click', testAnimate);

    const savedata = document.querySelector('#savedata');
    savedata.addEventListener('click', saveDemoDataToDisk);

    const loaddata = document.querySelector('#loaddata');
    loaddata.addEventListener('click', () => {
      const f = document.createElement('input');
      f.type = 'file';
      f.addEventListener('change', () => {
        const file = new FileReader(f.files[0]);
        file.addEventListener('load', () => {
          const json = JSON.parse(file.result);
          for (const frame of json) {
            processIncomingFrame(frame);
          }
        });
        file.readAsText(f.files[0]);
      });
      f.click();
    });

    const importtracedata = document.querySelector('#importtracebutton');
    importtracedata.addEventListener('click', () => {
      const f = document.createElement('input');
      f.type = 'file';
      f.addEventListener('change', () => {
        const file = new FileReader(f.files[0]);
        file.addEventListener('load', () => {
          const json = JSON.parse(file.result);
          const traceEvents = json.traceEvents;

          curr_frame = "0";
          curr_draws = [];
          sources_this_frame = []
          global_sources_mapping = []
          for (const event of traceEvents) {
            if(!event.cat.includes("viz.visual_debugger"))
              continue;

            if (event.name == "visual_debugger_sync") {
              single_frame = { "drawcalls": [], "frame": "none", "logs": [], "new_sources": [], "time": "0", "version": 1, "windowx": 2400, "windowy": 1600, "threads": [ {"thread_id": "1" , "thread_name": "allthreads"}]};
              single_frame.drawcalls = curr_draws;
              curr_frame = event.args.last_presented_trace_id;
              single_frame.frame = curr_frame;
              single_frame.new_sources = sources_this_frame;
              single_frame.windowx =  parseInt(event.args.display_size.split("x")[0]);
              single_frame.windowy =  parseInt(event.args.display_size.split("x")[1]);
              processIncomingFrame(single_frame);
              curr_draws = [];
              sources_this_frame = [];
            }
            else {
              const single_call = { "drawindex": curr_draws.length, "option": { "alpha": 5, "color": "#ff0000" }, "pos": [-1, -1], "size": [-1, -1], "source_index": -1,"thread_id":1, "buff_id":-1};

              single_call.pos[0] = parseFloat(event.args.args.pos_x);
              single_call.pos[1] = parseFloat(event.args.args.pos_y);
              single_call.size[0] = parseFloat(event.args.args.size_x);
              single_call.size[1] = parseFloat(event.args.args.size_y);
              single_call.text = event.args.args.text;

              found_source_index = global_sources_mapping.indexOf(event.name);
              if(found_source_index == -1)
              {
                new_source = { "anno": "frame.root.display_rect", "file": "none", "func": "none", "index": -1, "line": -1 };
                new_source.anno = event.name;
                global_sources_mapping.push(event.name);
                found_source_index = global_sources_mapping.indexOf(event.name);
                new_source.index = found_source_index;
                sources_this_frame.push(new_source);
              }

              single_call.source_index = found_source_index;
              curr_draws.push(single_call);
            }
          }
        });
        file.readAsText(f.files[0]);
      });
      f.click();
    });

    setUpPlayer();
    restoreFilters();
  }

  /**
   * Gets value from localStorage if it exists, and sets the <option> with that
   * id to be selected in options_el.
   * @param {string} key: The localStorage key to get if present.
   * @param {Element} options_el: The <select> Element to be updated.
   * @returns {boolean} Whether a value from localStorage was used to change the
   *     the selected option.
   */
  function getStoredOptionsValue(key, options_el) {
    let storedId = localStorage.getItem(key);
    if (storedId != null) {
      let option = options_el.namedItem(storedId);
      if (option == null) {
        console.error(`No option with id ${storedId} found on ${options_el.id}`);
        localStorage.removeItem(key);
      } else {
        options_el.selectedIndex = option.index;
        return true;
      }
    }

    return false;
  }

  function updateDrawScrubberSizes(minIndex, maxIndex, nDraws) {
    const scrubberMin = document.querySelector('#minDrawScrubber');
    const scrubberMax = document.querySelector('#maxDrawScrubber');
    const drawRange = document.querySelector('#drawRange');

    // The point where the sliders meet will be halway between the two values.
    // This means the slider you select when clicking will always be the one
    // closest to your mouse.
    let mid = Math.floor((minIndex + maxIndex) / 2);
    scrubberMin.max = mid;
    scrubberMax.min = mid;
    // Update the size of each slider to its fraction of the total range.
    scrubberMin.style = `flex-grow: ${mid}`;
    scrubberMax.style = `flex-grow: ${nDraws - mid}`;
  }

  function setDrawScrubbers(minIndex, maxIndex, nDraws) {
    const scrubberMin = document.querySelector('#minDrawScrubber');
    const scrubberMax = document.querySelector('#maxDrawScrubber');
    const drawRange = document.querySelector('#drawRange');

    scrubberMin.value = minIndex;
    scrubberMax.value = maxIndex;
    scrubberMax.max = nDraws;
    drawRange.textContent = ` [${minIndex}, ${maxIndex})`;

    updateDrawScrubberSizes(minIndex, maxIndex, nDraws);
  }

  function setUpPlayer() {
    // First, set up the viewer.
    const canvas = document.querySelector('#canvas');
    const logContainer = document.querySelector('#log');
    const viewer = new Viewer(canvas, logContainer);
    // Now create the player for the viewer.
    const player = new Player(viewer, (frame) => {
      // TODO: This feels like a hack. Find a cleaner way to update the scrubbers?
      const scrubberFrame = document.querySelector('#scrubberframe');
      scrubberFrame.value = player.currentFrameIndex;

      if (!frame) {
        // TODO: Not sure why frame is sometimes empty. Fix that.
        return;
      }

      setDrawScrubbers(
          frame.minIndex(), frame.maxIndex(), frame.submissionCount());
    });

    document.querySelector('#pause').addEventListener('click', () => {
      player.pause();
      pause.setAttribute('style', 'background:#000000;color:white');
    });

    document.querySelector('#play').addEventListener('click', () => {
      player.play();
      pause.removeAttribute('style');
    });

    document.querySelector('#prev').addEventListener('click', () => {
      player.rewind();
    });

    document.querySelector('#next').addEventListener('click', () => {
      player.forward();
    });

    document.querySelector('#live').addEventListener('click', () => {
      player.freezeFrame(DrawFrame.frameBuffer.numFrames - 1);
      player.live();
      has_disconnected = false;
      pause.removeAttribute('style');
    });

    const scrubberFrame = document.querySelector('#scrubberframe');
    scrubberFrame.addEventListener('input', () => {
      player.freezeFrame(parseInt(scrubberFrame.value));
      pause.setAttribute('style', 'background:#000000;color:white');
    });

    const scrubberDrawMin = document.querySelector('#minDrawScrubber');
    const scrubberDrawMax = document.querySelector('#maxDrawScrubber');
    function drawScrubberChanged() {
      let min = parseInt(scrubberDrawMin.value);
      let max = parseInt(scrubberDrawMax.value);
      player.freezeFrame(parseInt(scrubberFrame.value), min, max);
      updateDrawScrubberSizes(min, max, parseInt(scrubberDrawMax.max));
    }

    scrubberDrawMin.addEventListener('input', () => {
      drawScrubberChanged();
    });
    scrubberDrawMax.addEventListener('input', () => {
      drawScrubberChanged()
    });

    let currentMouseX = 0;
    let currentMouseY = 0;
    let zoom = 100;
    const viewerScale = document.querySelector("#viewerscale");

    function scaleChanged() {
      let selected = viewerScale.selectedOptions[0];
      if (selected.id != "freeCam") {
        player.setViewerScale(viewerScale.value);
        zoom = parseInt(viewerScale.value);
      }
      else {
        player.setViewerScale(zoom);
        canvas.addEventListener('mouseenter', () => {
          canvas.addEventListener('mousemove', function (event) {
            currentMouseX = Math.round(event.offsetX);
            currentMouseY = Math.round(event.offsetY);
          });
        });
        canvas.addEventListener('wheel', function(e) {
          e.preventDefault();
          var delta = e.deltaY;

          viewer.zoomToMouse(currentMouseX, currentMouseY, delta);

        }, {passive:false});
      }
    }
    const scaleKey = "scale";
    if (getStoredOptionsValue(scaleKey, viewerScale)) {
      scaleChanged();
    }
    viewerScale.addEventListener('input', () => {
      scaleChanged();
      localStorage.setItem(scaleKey, viewerScale.selectedOptions[0].id);
    });

    const viewerOrientation = document.querySelector("#viewerorientation");

    function orientationChanged() {
      let selected = viewerOrientation.selectedOptions[0];
      // Change dropdown style when setting non-zero orientation.
      if (selected.id != '0deg') {
        viewerOrientation.classList.add("nonzero-orientation");
      } else {
        viewerOrientation.classList.remove("nonzero-orientation");
      }
      player.setViewerOrientation(viewerOrientation.value);
    }
    const orientationKey = 'orientation';
    if (getStoredOptionsValue(orientationKey, viewerOrientation)) {
      orientationChanged();
    }
    viewerOrientation.addEventListener('change', () => {
      orientationChanged();
      localStorage.setItem(orientationKey, viewerOrientation.selectedOptions[0].id);
    });

  }


  function showModal(element, focusSelector) {
    const container = document.querySelector('.modalContainer');
    container.appendChild(element);
    container.style.display = 'block';
    element.querySelector(focusSelector).focus();
  }

  function hideModal() {
    const container = document.querySelector('.modalContainer');
    container.style.display = 'none';
    container.textContent = '';
  }

</script>

</html>
